package away3d.core.base ;


import away3d.core.math.MathConsts ;
import away3d.core.math.Matrix3DUtils ;
import away3d.core.math.Quaternion ;
import away3d.core.math.Vector3DUtils ;
import away3d.library.assets.IAsset ;
import away3d.library.assets.NamedAssetBase ;
import flash.events.EventDispatcher ;
import flash.geom.Matrix3D ;
import flash.geom.Vector3D ;



/**
 * Object3D provides a base class for any 3D object that has a ( local ) transformation.
 * 
 * Standard Transform:
 * - The standard order for transformation is [parent transform] * ( Translate+Pivot ) * ( Rotate ) * ( -Pivot ) * ( Scale ) * [child transform]
 *   - This is the order of matrix multiplications, left-to-right.
 *   - The order of transformation is right-to-left, however!
 *       ( Scale ) happens before ( -Pivot ) happens before ( Rotate ) happens before ( Translate+Pivot )
 *   - with no pivot, the above transform works out to [parent transform] * Translate * Rotate * Scale * [child transform] 
 *       ( Scale ) happens before ( Rotate ) happens before ( Translate )
 *   - This is based on code in updateTransform and ObjectContainer3D.updateSceneTransform().
 *   - Matrix3D prepend = operator on rhs - e.g. transform' = transform * rhs ;
 *   - Matrix3D append =  operator on lhr - e.g. transform' = lhs * transform ;
 * 
 * To affect Scale:
 * - set scaleX/Y/Z directly, or call scale( delta )
 * 
 * To affect Pivot:
 * - set pivotPoint directly, or call movePivot()
 * 
 * To affect Rotate:
 * - set rotationX/Y/Z individually ( using degrees ), set eulers [all 3 angles] ( using radians ), or call rotateTo()
 * - call pitch()/yaw()/roll()/rotate() to add an additional rotation *before* the current transform.  
 *     rotationX/Y/Z will be reset based on these operations.
 * 
 * To affect Translate ( post-rotate translate ):
 * - set x/y/z/position or call moveTo().
 * - call translate(), which modifies x/y/z based on a delta vector.
 * - call moveForward()/moveBackward()/moveLeft()/moveRight()/moveUp()/moveDown()/translateLocal() to add an
 *     additional translate *before* the current transform. x/y/z will be reset based on these operations.
 */
class Object3D extends NamedAssetBase
{
    
    
    /**
     * An object that can contain any extra data.
     */
    public var extra                    : Object ;
    private  var _transform             : Matrix3D ;
    private var _transformDirty         : Bool ;
    private var _rotationValuesDirty    : Bool ;
    private var _scaleValuesDirty       : Bool ;
    private var _rotationX              : Float ;
    private var _rotationY              : Float ;
    private var _rotationZ              : Float ;
    private var _eulers                 : Vector3D ;
    private  var _scaleX                : Float ;
    private  var _scaleY                : Float ;
    private  var _scaleZ                : Float ;
    private  var _pivotPoint            : Vector3D ;
    private  var _pivotZero             : Bool ;
    private var _lookingAtTarget        : Vector3D ;
    private var _flipY                  : Matrix3D ;
    
    // used for calculation holders:
    private static var _quaternion      : Quaternion ;
    private  var _x                     : Float ;
    private  var _y                     : Float ;
    private  var _z                     : Float ;
    
    /**
     * A calculation placeholder.
     */
    private  var _pos                   : Vector3D ;
    
    
    
    /**
     * Creates an Object3D object.
     */
    public function new()
    {
        
        _transform              = new Matrix3D() ;
        _transformDirty         = true ;
        _rotationX              = 0 ;
        _rotationY              = 0 ;
        _rotationZ              = 0 ;
        _eulers                 = new Vector3D() ;
        _scaleX                 = 1 ;
        _scaleY                 = 1 ;
        _scaleZ                 = 1 ;
        _pivotPoint             = new Vector3D() ;
        _pivotZero              = true ;
        _lookingAtTarget        = new Vector3D() ;
        _flipY                  = new Matrix3D() ;
        _quaternion             = new Quaternion() ;
        _x                      = 0 ;
        _y                      = 0 ;
        _z                      = 0 ;
        _pos                    = new Vector3D() ;
        
        _transform.identity() ;
        _flipY.appendScale( 1, -1, 1 ) ;
        
    }
    
    
    /**
     * The transformation of the 3d object, relative to the local coordinates of the parent <code>ObjectContainer3D</code>.
     */
    private function get_transform() : Matrix3D
    {
        
        if( _transformDirty )
        {
            
            updateTransform() ;
            
        }
        
        return _transform ;
        
    }
    
    
    private function set_transform( value : Matrix3D )
    {
        
        _transform.copyFrom( value ) ;
        
        _transformDirty         = false ;
        _rotationValuesDirty    = true ;
        _scaleValuesDirty       = true ;
        
        value.copyRowTo( 3, _pos ) ;
        
        _x                      = _pos.x ;
        _y                      = _pos.y ;
        _z                      = _pos.z ;
        
    }
    
    
    /**
     * Appends a uniform scale to the current transformation.
     * @param value The amount by which to scale.
     */
    public function scale( value : Float )
    {
        
        _scaleX *= value ;
        _scaleY *= value ;
        _scaleZ *= value ;
//            _scaleValuesDirty = false ;
        invalidateTransform() ;
    }
    
    
    /**
     * Moves the 3d object forwards along it's local z axis
     *
     * @param    distance    The length of the movement
     */
    public function moveForward( distance : Float )
    {
        
        translateLocal( Vector3D.Z_AXIS, distance ) ;
        
    }
    
    /**
     * Moves the 3d object backwards along it's local z axis
     *
     * @param    distance    The length of the movement
     */
    public function moveBackward( distance : Float )
    {
        
        translateLocal( Vector3D.Z_AXIS, -distance ) ;
        
    }
    
    
    /**
     * Moves the 3d object backwards along it's local x axis
     *
     * @param    distance    The length of the movement
     */
    public function moveLeft( distance : Float )
    {
        
        translateLocal( Vector3D.X_AXIS, -distance ) ;
        
    }
    
    
    /**
     * Moves the 3d object forwards along it's local x axis
     *
     * @param    distance    The length of the movement
     */
    public function moveRight( distance : Float )
    {
        
        translateLocal( Vector3D.X_AXIS, distance ) ;
        
    }
    
    
    /**
     * Moves the 3d object forwards along it's local y axis
     *
     * @param    distance    The length of the movement
     */
    public function moveUp( distance : Float )
    {
        
        translateLocal( Vector3D.Y_AXIS, distance ) ;
        
    }
    
    
    /**
     * Moves the 3d object backwards along it's local y axis
     *
     * @param    distance    The length of the movement
     */
    public function moveDown( distance : Float )
    {
        
        translateLocal( Vector3D.Y_AXIS, -distance ) ;
        
    }
    
    
    /**
     * Moves the 3d object directly to a point in space
     *
     * @param    dx        The amount of movement along the local x axis.
     * @param    dy        The amount of movement along the local y axis.
     * @param    dz        The amount of movement along the local z axis.
     */
    public function moveTo( dx : Float, dy : Float, dz : Float )
    {
        
        if( _x == dx && _y == dy && _z == dz ) return ;
        
        _x = dx ;
        _y = dy ;
        _z = dz ;
        
        invalidateTransform() ;
        
    }
    
    
    /**
     * Moves the local point around which the object rotates.
     *
     * @param    dx        The amount of movement along the local x axis.
     * @param    dy        The amount of movement along the local y axis.
     * @param    dz        The amount of movement along the local z axis.
     */
    public function movePivot( dx : Float, dy : Float, dz : Float )
    {
        
        _pivotPoint.x = dx ;
        _pivotPoint.y = dy ;
        _pivotPoint.z = dz ;
        
        invalidateTransform() ;
        
    }
    
    
    /**
     * Defines the local point around which the object rotates.
     */
    private function get_pivotPoint() : Vector3D
    {
        
        return _pivotPoint  ;
        
    }
    
    
    private function set_pivotPoint( pivot : Vector3D )
    {
        
        _pivotPoint = pivot.clone() ;
        _pivotZero  = ( _pivotPoint.x == 0 ) && ( _pivotPoint.y == 0 ) && ( _pivotPoint.z == 0 ) ;
        
        invalidateTransform() ;
        
    }
    
    
    /**
     * Moves the 3d object along a vector by a defined length
     *
     * @param    axis        The vector defining the axis of movement
     * @param    distance    The length of the movement
     */
    public function translate( axis : Vector3D, distance : Float )
    {
        
        var x       = axis.x ;
        var y       = axis.y, 
        var z       = axis.z ;
        var len     = distance / Math.sqrt( x * x + y * y + z * z ) ;
        _x          += x * len ;
        _y          += y * len ;
        _z          += z * len ;
        
        invalidateTransform() ;
        
    }
    
    
    /**
     * Moves the 3d object along a vector by a defined length
     *
     * @param    axis        The vector defining the axis of movement
     * @param    distance    The length of the movement
     */
    public function translateLocal( axis : Vector3D<Float>, distance : Float )
    {
        
        var x       = axis.x ; 
        var y       = axis.y ;
        var z       = axis.z ;
        var len     = distance / Math.sqrt( x * x + y * y + z * z ) ;
        
        transform.prependTranslation( x*len, y*len, z*len ) ;
        _transform.copyRowTo( 3, _pos ) ;
        _x          = _pos.x ;
        _y          = _pos.y ;
        _z          = _pos.z ;
        
    }
    
    
    /**
     * Defines the position of the 3d object, relative to the local coordinates of the parent <code>ObjectContainer3D</code>.
     */
    private function get_position() : Vector3D<Float>
    {
        
        transform.copyRowTo( 3, _pos ) ;
        return _pos ;
        
    }
    
    
    private function set_position( value : Vector3D<Float> )
    {
        
        _x = value.x ;
        _y = value.y ;
        _z = value.z ;
        invalidateTransform() ;
        
    }
    
    
    /**
     * Rotates the 3d object around it's local x-axis
     *
     * @param    angle        The amount of rotation in degrees
     */
    public function pitch( angle : Float )
    {
        
        rotate( Vector3D.X_AXIS, angle ) ;
        
    }
    
    
    /**
     * Rotates the 3d object around it's local y-axis
     *
     * @param    angle        The amount of rotation in degrees
     */
    public function yaw( angle : Float )
    {
        
        rotate( Vector3D.Y_AXIS, angle ) ;
        
    }
    
    
    /**
     * Rotates the 3d object around it's local z-axis
     *
     * @param    angle        The amount of rotation in degrees
     */
    public function roll( angle : Float )
    {
        
        rotate( Vector3D.Z_AXIS, angle ) ;
        
    }
    
    
    public function clone() : Object3D
    {
        
        var clone           = new Object3D() ;
        clone.pivotPoint    = pivotPoint ;
        clone.transform     = transform ;
        // todo: implement for all subtypes
        return clone ;
        
    }
    
    
    /**
     * Rotates the 3d object directly to a euler angle
     *
     * @param    ax        The angle in degrees of the rotation around the x axis.
     * @param    ay        The angle in degrees of the rotation around the y axis.
     * @param    az        The angle in degrees of the rotation around the z axis.
     */
    public function rotateTo( ax : Float, ay : Float, az : Float )
    {
        
        _rotationX              = ax * MathConsts.DEGREES_TO_RADIANS ;
        _rotationY              = ay * MathConsts.DEGREES_TO_RADIANS ;
        _rotationZ              = az * MathConsts.DEGREES_TO_RADIANS ;
        _rotationValuesDirty    = false ;
        
        invalidateTransform() ;
        
    }
    
    
    /**
     * Rotates the 3d object around an axis by a defined angle
     *
     * @param    axis        The vector defining the axis of rotation
     * @param    angle        The amount of rotation in degrees
     */
    public function rotate( axis : Vector3D, angle : Float )
    {
        
        // notify
        invalidateTransform() ;
        
        axis.normalize() ;
        
        transform.prependRotation( angle, axis ) ;
        
        _rotationValuesDirty = true ;
        
    }
    
    
    /**
     * Rotates the 3d object around to face a point defined relative to the local coordinates of the parent <code>ObjectContainer3D</code>.
     *
     * @param    target        The vector defining the point to be looked at
     * @param    upAxis        An optional vector used to define the desired up orientation of the 3d object after rotation has occurred
     */
    public function lookAt( target : Vector3D, ?upAxis : Vector3D = null )
    {
        
        _lookingAtTarget = target ;
        
        var yAxis   : Vector3D<Float> ; 
        var zAxis   : Vector3D<Float> ;
        var xAxis   : Vector3D<Float> ;
        var raw     : Vector<Float> ;
        
        //TODO: Change this...
        upAxis ||= Vector3D.Y_AXIS ;
        
        zAxis = target.subtract( position ) ;
        zAxis.normalize() ;
        
        xAxis = upAxis.crossProduct( zAxis ) ;
        xAxis.normalize() ;
        
//            if( xAxis.length < .05 ) {
//                xAxis = upAxis.crossProduct( Vector3D.Z_AXIS ) ;
//            }
        
        yAxis   = zAxis.crossProduct( xAxis ) ;
        raw     = Matrix3DUtils.RAW_DATA_CONTAINER ;
        
        _transform.copyRawDataTo( raw ) ;
        
        //TODO: hope we can forget uints here.
        raw[ 0  ] = _scaleX*xAxis.x ;
        raw[ 1  ] = _scaleX*xAxis.y ;
        raw[ 2  ] = _scaleX*xAxis.z ;
        
        raw[ 4  ] = _scaleY*yAxis.x ;
        raw[ 5  ] = _scaleY*yAxis.y ;
        raw[ 6  ] = _scaleY*yAxis.z ;
        
        raw[ 8  ] = _scaleZ*zAxis.x ;
        raw[ 9  ] = _scaleZ*zAxis.y ;
        raw[ 10 ] = _scaleZ*zAxis.z ;
        
        _transform.copyRawDataFrom( raw ) ;
        
        _rotationValuesDirty = true ;
        
    }
    
    
    public var x( get_x, set_x ) : Float ;
    
    
    /**
     * Defines the x coordinate of the 3d object relative to the local coordinates of the parent <code>ObjectContainer3D</code>.
     */
    private function get_x() : Float
    {
        
        return _x ;
        
    }
    
    
    private function set_x( value : Float ) : Float
    {
        
        if( _x == value ) return value ;
        if( value != value ) throw new Error( "isNaN( x )" ) ;
        _x = value ;
        invalidateTransform() ;
        
        return value ;
        
    }
    
    
    public var y( get_y, set_y ) : Float ;
    
    
    /**
     * Defines the y coordinate of the 3d object relative to the local coordinates of the parent <code>ObjectContainer3D</code>.
     */
    private function get_y() : Float
    {
        return _y ;
    }
    
    
    private function set_y( value : Float ) : Float
    {
        if( _y == value ) return ;
        if( !( value > 0 ) && !( value <= 0 ) ) throw new Error( "isNaN( x )" ) ;
        _y = value ;
        invalidateTransform() ;
        
        return value ;
        
    }
    
    
    public var z( get_z, set_z ) : Float ;
    
    
    /**
     * Defines the z coordinate of the 3d object relative to the local coordinates of the parent <code>ObjectContainer3D</code>.
     */
    private function get_z() : Float
    {
        return _z ;
    }
    
    
    private function set_z( value : Float ) : Float
    {
        if( _z == value ) return ;
        if( !( value > 0 ) && !( value <= 0 ) ) throw new Error( "isNaN( x )" ) ;
        _z = value ;
        invalidateTransform() ;
        
        return value ;
        
    }
    
    
    public var rotationX( get_rotationX, set_rotationX ) : Float ;
    
    
    /**
     * Defines the euler angle of rotation of the 3d object around the x-axis, relative to the local coordinates of the parent <code>ObjectContainer3D</code>.
     */
    private function get_rotationX() : Float
    {
        
        if( _rotationValuesDirty ) updateTransformValues() ;
        
        return _rotationX * MathConsts.RADIANS_TO_DEGREES ;
        
    }
    
    
    private function set_rotationX( rot : Float ) : Float
    {
        
        if( rotationX == rot ) return ;
        
        _rotationX = rot * MathConsts.DEGREES_TO_RADIANS ;
        
        invalidateTransform() ;
        
        return rot ;
        
    }
    
    
    public var rotation( get_rotationY, set_rotationY ) : Float ;
    
    
    /**
     * Defines the euler angle of rotation of the 3d object around the y-axis, relative to the local coordinates of the parent <code>ObjectContainer3D</code>.
     */
    private function get_rotationY() : Float
    {
        
        if( _rotationValuesDirty ) updateTransformValues() ;
        
        return _rotationY * MathConsts.RADIANS_TO_DEGREES ;
        
    }
    
    
    private function set_rotationY( rot : Float )
    {
        
        if( rotationY == rot ) return ;
        
        _rotationY = rot * MathConsts.DEGREES_TO_RADIANS ;
        
        invalidateTransform() ;
        
    }
    
    
    public var rotationZ( get_rotationZ, set_rotationZ ) : Float ;
    
    
    /**
     * Defines the euler angle of rotation of the 3d object around the z-axis, relative to the local coordinates of the parent <code>ObjectContainer3D</code>.
     */
    private function get_rotationZ() : Float
    {
        
        if( _rotationValuesDirty ) updateTransformValues() ;
        
        return _rotationZ * MathConsts.RADIANS_TO_DEGREES ;
        
    }
    
    
    private function set_rotationZ( rot : Float )
    {
        
        if( rotationZ == rot ) return ;
        
        _rotationZ = rot * MathConsts.DEGREES_TO_RADIANS ;
        
        invalidateTransform() ;
        
    }
    
    
    public var scaleX( get_scaleX, set_scaleX ) : Float ;
    
    
    /**
     * Defines the scale of the 3d object along the x-axis, relative to local coordinates.
     */
    private function get_scaleX() : Float
    {
        if( _scaleValuesDirty ) updateTransformValues() ;
        return _scaleX ;
    }
    
    
    private function set_scaleX( scale : Float )
    {
        
        if( scaleX == scale ) return ;
        
        _scaleX = scale ;
        
        invalidateTransform() ;
        
    }
    
    
    public var scaleY( get_scaleY, set_scaleY ) : Float ;
    
    
    /**
     * Defines the scale of the 3d object along the y-axis, relative to local coordinates.
     */
    private function get_scaleY() : Float
    {
        if( _scaleValuesDirty ) updateTransformValues() ;
        return _scaleY ;
    }
    
    
    private function set_scaleY( scale : Float )
    {
        
        if( scaleY == scale ) return ;
        
        _scaleY = scale ;
        
        invalidateTransform() ;
        
    }
    
    
    public var scaleZ( get_scaleZ, set_scaleZ ) : Float ;
    
    
    /**
     * Defines the scale of the 3d object along the z-axis, relative to local coordinates.
     */
    private function get_scaleZ() : Float
    {
        
        if( _scaleValuesDirty ) updateTransformValues() ;
        
        return _scaleZ ;
        
    }
    
    
    private function set_scaleZ( scale : Float )
    {
        
        if( scaleZ == scale ) return ;
        
        _scaleZ = scale ;
        
        _transformDirty = true ;
        
        invalidateTransform() ;
        
    }
    
    
    public var eulers( get_eulers, set_eulers ) : Vector3D<Float> ;
    
    
    /**
     * Defines the rotation of the 3d object as a <code>Vector3D</code> object containing euler angles for rotation around x, y and z axis.
     */
    private function get_eulers() : Vector3D<Float>
    {
        
        if( _rotationValuesDirty ) updateTransformValues() ;
        
        _eulers.x = _rotationX * MathConsts.RADIANS_TO_DEGREES ;
        _eulers.y = _rotationY * MathConsts.RADIANS_TO_DEGREES ;
        _eulers.z = _rotationZ * MathConsts.RADIANS_TO_DEGREES ;
        
        return _eulers ;
        
    }
    
    
    private function set_eulers( value : Vector3D<Float> )
    {
        
        _rotationX = value.x * MathConsts.RADIANS_TO_DEGREES ;
        _rotationY = value.y * MathConsts.RADIANS_TO_DEGREES ;
        _rotationZ = value.z * MathConsts.RADIANS_TO_DEGREES ;
        _rotationValuesDirty = false ;
        invalidateTransform() ;
        
    }
    
    
    /**
     * Cleans up any resources used by the current object.
     * @param deep Indicates whether other resources should be cleaned up, that could potentially be shared across different instances.
     */
    public function dispose( deep : Bool )
    {
        
        
        
    }
    
    
    /**
     * Invalidates the transformation matrix, causing it to be updated upon the next request
     */
    private  function invalidateTransform()
    {
        
        _transformDirty = true ;
        
    }
    
    
    private  function updateTransform()
    {
        
        if( _rotationValuesDirty || _scaleValuesDirty ) updateTransformValues() ;
        
        _quaternion.fromEulerAngles( _rotationY, _rotationZ, -_rotationX ) ; // Swapped
        
        if( _pivotZero ) 
        {
            Matrix3DUtils.quaternion2matrix( _quaternion, _transform ) ;
            _transform.prependScale( _scaleX, _scaleY, _scaleZ ) ;
            _transform.appendTranslation( _x, _y, _z ) ;
            
        }
        else
        {
            
            _transform.identity() ;
            _transform.appendTranslation( -_pivotPoint.x, -_pivotPoint.y, -_pivotPoint.z ) ;
            _transform.append( Matrix3DUtils.quaternion2matrix( _quaternion ) ) ;
            _transform.appendTranslation( _x + _pivotPoint.x, _y + _pivotPoint.y, _z + _pivotPoint.z ) ;
            _transform.prependScale( _scaleX, _scaleY, _scaleZ ) ;
            
        }
        
        _transformDirty = false ;
        
    }
    
    
    private function updateTransformValues()
    {
        
        var raw : Vector<Float> ;
        var rot : Vector3D<Float> ;
        var x : Float ; 
        var y : Float ; 
        var z : Float ;
        
        if( _rotationValuesDirty )
        {
            
//                _quaternion.fromMatrix( _transform ) ;
            rot = Vector3DUtils.matrix2euler( _transform ) ;
            _rotationX = rot.x ;
            _rotationY = rot.y ;
            _rotationZ = rot.z ;
            _rotationValuesDirty = false ;
            
        }
        
        if( _scaleValuesDirty )
        {
            
            raw = Matrix3DUtils.RAW_DATA_CONTAINER ;
            _transform.copyRawDataTo( raw ) ;
            x = raw[ 0  ] ;
            y = raw[ 1  ] ;
            z = raw[ 2  ] ;
            _scaleX = Math.sqrt( x * x + y * y + z * z ) ;
            x = raw[ 4  ] ;
            y = raw[ 5  ] ;
            z = raw[ 6  ] ;
            _scaleY = Math.sqrt( x * x + y * y + z * z ) ;
            x = raw[ 8  ] ;
            y = raw[ 9  ] ;
            z = raw[ 10 ] ;
            _scaleZ = Math.sqrt( x * x + y * y + z * z ) ;
            _scaleValuesDirty = false ;
            
        }
        
    }
    
    
}